/*
 * Copyright (c) 2020 - present, Inspur Genersoft Co., Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.iec.edp.caf.core.session;

import io.iec.edp.caf.commons.runtime.CafEnvironment;
import io.iec.edp.caf.commons.utils.SpringBeanUtils;
import io.iec.edp.caf.core.session.constants.SessionConstants;
import io.iec.edp.caf.core.session.properties.CAFBootBSessionConfigurationProperties;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.log.LogMessage;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Constructor;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Session管理类
 *
 * @author wangyandong
 * @date 2019/12/14 15:37
 */
public class CafSessionManager {

    private static final Log logger = LogFactory.getLog(CafSessionManager.class);
    private static final String BackendSession_Attribute_Name = "BACKEND_SESSION";
    private static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";

    //Spring的SessionRepo
    private final SessionRepository repo;

    //扩展的所有SessionCreator
    private final List<SessionCreator> creators;

    //CAF-Session过期判断使用的redisTemplate
    private RedisTemplate redisTemplate;

    //redis中session是否存在的lua判断
    private final static DefaultRedisScript SessionExistRedisScript = new DefaultRedisScript<>(
            "local expires = ''\n" +
                    "for i=1, #KEYS do\n" +
                    "  if(redis.call('EXISTS',KEYS[i]) == 0)\n" +
                    "  then\n" +
                    "    expires = expires..';'..KEYS[i]\n" +
                    "  end\n" +
                    "end\n" +
                    "return expires", String.class
    );

    /**
     * 构造函数
     *
     * @param repo SessionRepository
     */
    public CafSessionManager(SessionRepository repo) {
        this.repo = repo;
        this.creators = this.loadSessionCreator();
    }

    //加载所有SessionCreator的实现
    private List<SessionCreator> loadSessionCreator() {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        List<String> creatorNames = SpringFactoriesLoader.loadFactoryNames(SessionCreator.class, classLoader);
        //去重
        creatorNames = creatorNames.stream().distinct().collect(Collectors.toList());

        List<SessionCreator> creators = new ArrayList<>();
        for (String creatorName : creatorNames) {
            try {
                Constructor<?> constructor = ClassUtils.forName(creatorName, classLoader).getDeclaredConstructor();
                ReflectionUtils.makeAccessible(constructor);
                creators.add((SessionCreator) constructor.newInstance());
            } catch (Throwable ex) {
                logger.trace(LogMessage.format("Failed to load %s", creatorName), ex);
            }
        }
        return creators;
    }

    //创建CafSession
    public CafSession create(Integer tenantId, String userId, String language, SessionType sessionType) {
        return this.create(tenantId, userId, "", "", language, null, sessionType);
    }

    /**
     * 创建CafSession
     *
     * @param tenantId    租户标识
     * @param userId      用户标识
     * @param userCode    用户编号
     * @param userName    用户名称
     * @param language    语言
     * @param items       存储信息
     * @param sessionType Session类型
     * @return CafSession
     */
    public CafSession create(Integer tenantId, String userId, String userCode, String userName, String language, HashMap<String, String> items, SessionType sessionType) {

        if (sessionType != SessionType.backend)
            throw new RuntimeException("This method does not support WebSession");

        //创建Spring内部的Session
        Session session = this.repo.createSession();
        CAFBootBSessionConfigurationProperties properties = SpringBeanUtils.getBean(CAFBootBSessionConfigurationProperties.class);
        session.setMaxInactiveInterval(Duration.ofSeconds(properties.getTimeout()));

        //构造CafSession
        SessionCreator sc = this.getMatchingSesscionCreator(sessionType);
        if (sc == null)
            throw new RuntimeException(String.format("not supported sesstionType:%s", sessionType));

        CafSession cafSession = sc.create(session.getId(), tenantId, userId, userCode, userName, language, items);

        //创建SecurityContext
        SecurityContext context = createSecurityContext();
        context.setAuthentication(cafSession);

        //塞入CafSession
        session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);

        //保存Session
        this.repo.save(session);

        return cafSession;
    }

    /**
     * 根据id查询CafSession
     *
     * @param sessionId Session的id
     * @return CafSession
     */
    public CafSession findById(String sessionId) {
        Assert.hasText(sessionId, "sessionId is not null");
        Session session = this.repo.findById(sessionId);
        if (session == null)
            return null;

        if (session.getAttributeNames().contains(SPRING_SECURITY_CONTEXT_KEY)) {
            SecurityContext context = session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
            return (CafSession) context.getAuthentication();
        }

        return null;
    }

    /**
     * 更新CafSession
     *
     * @param cafSession CafSession
     */
    public void update(CafSession cafSession) {
        Assert.notNull(cafSession, "session is null");

        Session session = this.repo.findById(cafSession.getId());
        if (session == null) {
            //创建Spring内部的Session
            session = this.repo.createSession();
            session.setMaxInactiveInterval(Duration.ofDays(30));
            //创建SecurityContext
            SecurityContext context = createSecurityContext();
            context.setAuthentication(cafSession);

            //塞入CafSession
            session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
        } else {
            if (session.getAttributeNames().contains(SPRING_SECURITY_CONTEXT_KEY)) {
                SecurityContext context = session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
                context.setAuthentication(cafSession);

                //塞入CafSession(不执行setAttr会导致session不更新)
                session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
            }
        }
        //保存Session
        this.repo.save(session);
    }

    /**
     * 根据id删除session
     *
     * @param sessionId Session的id
     */
    public void deleteById(String sessionId) {
        Assert.hasText(sessionId, "sessionId is not null");
        this.repo.deleteById(sessionId);
    }

    /**
     * 根据id判断Session是否过期
     *
     * @param sessionId Session的id
     * @return boolean
     */
    public boolean isExpired(String sessionId) {
        Assert.hasText(sessionId, "sessionId is not null");

        Session session = this.repo.findById(sessionId);
        if (session == null)
            return true;

        return session.isExpired();
    }

    /**
     * 批量判断session是否存在
     *
     * @param sessionIds session的id
     * @return 是否存在
     */
    public List<String> filterExpired(List<String> sessionIds) {
        List<String> expires = null;
        if (CafEnvironment.getEnvironment().getProperty("redis.enabled", Boolean.class, true) && getRedisTemplate() != null) {
            List<String> keys = sessionIds.stream().map(x -> SessionConstants.SESSION_REDIS_NAMESPACE + ":sessions:" + x).collect(Collectors.toList());
            //启用redis
            String result = (String) this.redisTemplate.execute(SessionExistRedisScript, keys);
            if (result != null && result.length() > 1) {
                result = result.substring(1);
                expires = Arrays.asList(result.split(";"));
                expires = expires.stream().map(x -> x.substring((SessionConstants.SESSION_REDIS_NAMESPACE + ":sessions:").length())).collect(Collectors.toList());
            }
        } else {
            //禁用redis
            for (String sessionId : sessionIds) {
                if (isExpired(sessionId)) {
                    if (expires == null) expires = new ArrayList<>();
                    expires.add(sessionId);
                }
            }
        }
        return expires;
    }

    /**
     * 返回操作caf-session的redisTemplate
     *
     * @return RedisTemplate
     */
    private RedisTemplate getRedisTemplate() {
        if (this.redisTemplate == null) {
            RedisConnectionFactory connectionFactory = SpringBeanUtils.getBean(RedisConnectionFactory.class);
            RedisTemplate<String, Object> template = new RedisTemplate();
            template.setConnectionFactory(connectionFactory);
            template.setKeySerializer(new StringRedisSerializer());     //单独指定key序列化方式
            template.setValueSerializer(new StringRedisSerializer());   //单独指定value序列化方式
            template.afterPropertiesSet();
            this.redisTemplate = template;
        }
        return this.redisTemplate;
    }

    /**
     * 创建安全上下文SecurityContext（内部存储认证授权的相关信息）
     *
     * @return SecurityContext
     */
    private SecurityContext createSecurityContext() {
        return SecurityContextHolder.createEmptyContext();
    }

    /**
     * 根据session类型查找构造器
     *
     * @param sessionType Session类型
     * @return Session构造器
     */
    private SessionCreator getMatchingSesscionCreator(SessionType sessionType) {
        return this.creators.stream()
                .filter(creator -> creator.getMatchingSessionTypes().contains(sessionType))
                .findAny().orElse(null);
    }
}
